package joe;

import java.util.ArrayList;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class SceneNode {
	private transient @Nullable SceneNode parent;
	private @Nullable ArrayList<@NotNull SceneNode> children;
	private @NotNull String name;
	private float x, y, z; // position
	private float rx, ry, rz; // rotate (euler angle, 360-degree, order:rz/roll,rx/pitch,ry/yaw, counter-clockwise towards the axis)
	private float sx = 1, sy = 1, sz = 1; // scale
	private final transient @NotNull Mat4 modelMat = new Mat4().identity();
	private transient byte dirty;
	private boolean disabled; // will skip onUpdate and onRender in main loop, includes its children

	public SceneNode(@Nullable String name) {
		this.name = name != null ? name : "";
	}

	public final @NotNull String getName() {
		return name;
	}

	public final void setName(@Nullable String name) {
		this.name = name != null ? name : "";
	}

	public float getX() {
		return x;
	}

	public float getY() {
		return y;
	}

	public float getZ() {
		return z;
	}

	public float getRotateX() {
		return rx;
	}

	public float getRotateY() {
		return ry;
	}

	public float getRotateZ() {
		return rz;
	}

	public float getScaleX() {
		return sx;
	}

	public float getScaleY() {
		return sy;
	}

	public float getScaleZ() {
		return sz;
	}

	public @NotNull SceneNode setX(float x) {
		this.x = x;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setY(float y) {
		this.y = y;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setZ(float z) {
		this.z = z;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setRotateX(float rx) {
		this.rx = rx;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setRotateY(float ry) {
		this.ry = ry;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setRotateZ(float rz) {
		this.rz = rz;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setScaleX(float sx) {
		this.sx = sx;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setScaleY(float sy) {
		this.sy = sy;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setScaleZ(float sz) {
		this.sz = sz;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setPosition(float x, float y, float z) {
		this.x = x;
		this.y = y;
		this.z = z;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setRotate(float rx, float ry, float rz) {
		this.rx = rx;
		this.ry = ry;
		this.rz = rz;
		markDirty();
		return this;
	}

	public @NotNull SceneNode setScale(float sx, float sy, float sz) {
		this.sx = sx;
		this.sy = sy;
		this.sz = sz;
		markDirty();
		return this;
	}

	public @NotNull Mat4 getModelMat() {
		return modelMat;
	}

	public int getDirty() {
		return dirty;
	}

	protected void markDirty() {
		dirty = 2;
		markParentDirty();
	}

	protected void markParentDirty() {
		for (var p = this; (p = p.parent) != null && p.dirty == 0; )
			p.dirty = 1;
	}

	protected void updateModelMat() {
		var c = children;
		if (c != null) {
			for (int i = 0, n = c.size(); i < n; i++) {
				var child = c.get(i);
				int d = child.dirty;
				if (d == 1)
					child.updateModelMat();
				else if (d == 2)
					child.updateModelMat(modelMat);
			}
		}
		dirty = 0;
	}

	protected void updateModelMat(@Nullable Mat4 parentModelMat) {
		var mm = modelMat;
		mm.transform(parentModelMat, x, y, z, rx, ry, rz, sx, sy, sz);
		var c = children;
		if (c != null) {
			for (int i = 0, n = c.size(); i < n; i++)
				c.get(i).updateModelMat(mm);
		}
		dirty = 0;
	}

	public final boolean isEnabled() {
		return !disabled;
	}

	public final void setEnabled(boolean enabled) {
		var disabled = !enabled;
		if (this.disabled != disabled) {
			this.disabled = disabled;
			onEnableChanged();
		}
	}

	public final void enable() {
		setEnabled(true);
	}

	public final void disable() {
		setEnabled(false);
	}

	public final @Nullable SceneNode parent() {
		return parent;
	}

	public final boolean isRoot() {
		return this instanceof Engine.SceneRoot;
	}

	public final int childrenSize() {
		var c = children;
		return c != null ? children.size() : 0;
	}

	public final @NotNull SceneNode getChildNode(int index) {
		var c = children;
		if (c == null)
			throw new IndexOutOfBoundsException("empty children");
		return c.get(index);
	}

	public final @Nullable SceneNode getChildNode(@NotNull String name) {
		var c = children;
		if (c != null) {
			for (int i = 0, n = c.size(); i < n; i++) {
				var node = c.get(i);
				if (node.name.equals(name))
					return node;
			}
		}
		return null;
	}

	public final void foreachChild(@NotNull Consumer<SceneNode> consumer) {
		var c = children;
		if (c != null) {
			for (int i = 0; i < c.size(); i++)
				consumer.accept(c.get(i));
		}
	}

	public final @NotNull SceneNode addChild(@NotNull SceneNode child) {
		child.link(this);
		return this;
	}

	public final boolean removeChild(@NotNull SceneNode child) {
		if (child.parent != this)
			return false;
		child.unlink();
		return true;
	}

	public final void removeAllChildren() {
		var c = children;
		if (c != null) {
			for (int i; (i = c.size() - 1) >= 0; )
				c.get(i).unlink();
		}
	}

	public final void link(@NotNull SceneNode newParent) {
		link(newParent, -1);
	}

	public final void link(@NotNull SceneNode newParent, int index) {
		var parentChildren = newParent.children;
		var n = parentChildren != null ? parentChildren.size() : 0;
		if (index > n)
			throw new IndexOutOfBoundsException("index=" + index + " > " + n);
		if (parentChildren == null)
			newParent.children = parentChildren = new ArrayList<>();
		if (parent != newParent) {
			unlink();
			parent = newParent;
			if (index >= 0)
				parentChildren.add(index, this);
			else
				parentChildren.add(this);
			if (dirty != 0)
				markParentDirty();
			onAdded();
		} else if (index >= 0) {
			int oldIndex = parentChildren.indexOf(this);
			if (oldIndex != index) {
				parentChildren.remove(oldIndex);
				if (oldIndex < index)
					index--;
				parentChildren.add(index, this);
			}
		}
	}

	public final boolean unlink() {
		var p = parent;
		if (p == null)
			return false;
		var c = p.children;
		if (c != null)
			c.remove(this);
		parent = null;
		onRemoved();
		return true;
	}

	@Override
	public final boolean equals(@Nullable Object obj) {
		return this == obj;
	}

	@SuppressWarnings("EmptyMethod")
	@Override
	public final int hashCode() {
		return super.hashCode();
	}

	protected void onAdded() {
	}

	@SuppressWarnings("EmptyMethod")
	protected void onRemoved() {
	}

	@SuppressWarnings("EmptyMethod")
	protected void onEnableChanged() {
	}

	protected void onUpdate() {
		var c = children;
		if (c != null) {
			for (int i = 0; i < c.size(); i++) {
				var child = c.get(i);
				if (!child.disabled)
					child.onUpdate();
			}
		}
	}

	protected void onRender(@NotNull Render render) {
		var c = children;
		if (c != null) {
			for (int i = 0; i < c.size(); i++) {
				var child = c.get(i);
				if (!child.disabled)
					child.onRender(render);
			}
		}
	}
}
